# -*- coding: utf-8 -*-
# @Author  : User1396529
# @Date    : 2023-11-19
# @Version : 0.1.4

""" 银行顾客跑路的仿真示例

涵盖的内容：

- 多服务台并联
- 排队系统
- 条件事件

场景：

一组服务时间随机的柜台，可能顾客因等待时间过长而离开。

基于 SimPy 2 教程的 TheBank 教程的程序 bank08.py 改进

更新记录：

  经过更新后的代码包含两个类：

  - BankSimulator 银行模拟器类
  - queue_info 仿真结果信息类

  排队信息保存为 DataFrame，传递给 queue_info 的 __init__
  把输出结果变成一个“输出结果类”，对这个类进行实例化之后，
  只要对这个类的属性和方法进行调用，就可以实现仿真结果的查看和绘图。

  ---

  2023-11-25:

  将创建进程的 env.process 移动到 BankSimulator 的 __init__
  方法下面，省去了在外部创建进程的不必要的过程。
"""

# In[0]:
# 导入工具包库
# 数据分析工具
import numpy as np
import pandas as pd

# 数据可视化工具
import matplotlib.pyplot as plt
from pylab import mpl
# 指定默认字体：解决plot不能显示中文问题
mpl.rcParams[
    'font.sans-serif'
    ] = ['SimHei'] 
mpl.rcParams[
    'axes.unicode_minus'
    ] = False # 解决保存图像是负号'-'显示为方块的问题
import seaborn as sns

# 内建工具包
import random # 随机数生成工具

# simpy 仿真工具
import simpy  

# In[2]:
# 定义银行模拟器类
class BankSimulator:
    """ 银行模拟器类

        项目的总类，包含如下方法：

        - __init__ 初始化类
        - source 顾客源，定时生成顾客
        - record_queue_length 记录仿真过程中各个时刻的队长
        - customer 顾客抵达，顾客接受服务，顾客离开
        - result 返回仿真结果各项参数
    """
    
    def __init__(
            self, env, 
            num_customer,
            num_counter,
            interval,
            time_in_bank,
            max_patience,
            min_patience,
            vip_rate=0.1):
        
        self.env = env
        
        self.vip_percent = (1-vip_rate) * 10
        
        self.wait_time_list = [
                [], # 用于保留顾客编号
                [], # 用于保存顾客的排队时间
                [], # 用于保存顾客是否因耐心耗尽而离开
                [], # 用于记录顾客是不是 VIP
            ]
        self.queue_len_list = [
                [], # 时间戳
                []  # 各个时间下的队列长度
            ]
        self.counter = simpy.PriorityResource(
                env, capacity=num_counter)
        
        # 这个变量表示是否保持一直记录队长
        # 默认为 True
        self.keep_recording_quelen = True 

        self.env.process(
            self.source(
                num_customer,
                interval,
                time_in_bank,
                max_patience,
                min_patience
            )
        )
        self.env.process(
            self.record_queue_length()
            )

    def source(
            self, 
            number, 
            interval, 
            time_in_bank, 
            max_patience, 
            min_patience):
        """ 顾客源：定时随机生成顾客
        """
        self.env.process(
                self.record_queue_length()
        )
        for i in range(number):
            c = self.customer(
                # 'Customer%02d' % i, 
                # 何必多此一举声明这是 “顾客”
                # 谁不知道这是 “顾客” 啊
                str(i),
                time_in_bank=time_in_bank,
                max_patience=max_patience,
                min_patience=min_patience
                )
            self.env.process(c) # 把函数保存为变量传入 env.process()
            t = random.expovariate(1.0 / interval)
            # 时间符合负指数分布
            yield self.env.timeout(t)
        self.keep_recording_quelen = False
        # 完成顾客计数循环后，停止记录队列长度
 
    def record_queue_length(self):
        """ 记录队列长度
        """
        while self.keep_recording_quelen:
            self.queue_len_list[0].append(self.env.now)
            self.queue_len_list[1].append(len(self.counter.queue))
            yield self.env.timeout(1)  # 每隔一定时间记录一次
   
    def customer(
            self, name, 
            time_in_bank, 
            max_patience, 
            min_patience):
        """ 顾客方法
            顾客抵达，顾客接受服务，顾客离开
        """

        # 生成排队优先级
        # 随机选择十分之一的顾客为 VIP
        is_VIP = (random.randint(1, 10) > self.vip_percent)
        # 在 SimPy 中，优先级队列的处理方式是：优先级值越小，优先级越高
        if is_VIP:
            priority = 0 
        else:
            priority = 1
        
        arrive = self.env.now
        print('%7.4f %s: Here I am' % (arrive, name))
    
        with self.counter.request(priority=priority) as req:
            patience = random.uniform(min_patience, max_patience)
            # Wait for the counter or abort at the end of our tether
            results = yield req | self.env.timeout(patience)
    
            wait = self.env.now - arrive
            self.wait_time_list[0].append(name)
            self.wait_time_list[1].append(wait) # 记录等待时间
            self.wait_time_list[3].append(is_VIP)
    
            if req in results:
                # 到了柜台之后
                self.wait_time_list[2].append(False)
                print('%7.4f %s: Waited %6.3f' % (
                    self.env.now, name, wait))
    
                tib = random.expovariate(1.0 / time_in_bank)
                yield self.env.timeout(tib)
                print('%7.4f %s: Finished' % (self.env.now, name))
    
            else:
                # 或者跑了
                self.wait_time_list[2].append(True)
                print('%7.4f %s: RENEGED after %6.3f' % (
                    self.env.now, name, wait
                    ))

    def result(self):
        """ 定义一个输出结果的方法

            返回值：

            - queue_info 类，也就是“排队结果信息类”，在下面定义
        """
        wait_time_df = pd.DataFrame([])
        wait_time_df['Customer'] = self.wait_time_list[0]
        wait_time_df['wait_time'] = self.wait_time_list[1]
        wait_time_df['is_reneged'] = self.wait_time_list[2]
        wait_time_df['is_VIP'] = self.wait_time_list[3]
        queue_len_df = pd.DataFrame(
                np.array(self.queue_len_list).T,
                columns=['time', 'queue_len']
                )
        return queue_info(self.env, wait_time_df, queue_len_df)

# In[3]:
# 定义排队结果信息类
class queue_info:
    """ 排队结果信息类

        包含如下的方法：

        - __init__ 初始化类
        - info 返回两张表：各个顾客的等待时间以及是否 reneged，
               以及各个时刻的队长
        - print_info 打印两张表的排队信息
        - plot_info 把排队参数画成图
    """
    def __init__(self, env, wait_time_df, queue_len_df):
        """ 类的初始化
        """
        self.env = env
        self.wait_time_df = wait_time_df
        self.queue_len_df = queue_len_df

    def info(self):
        """ 排队系统的参数信息
        """
        return self.wait_time_df, self.queue_len_df

    def print_info(self):
        """ 打印排队信息表
        """
        print(self.wait_time_df)
        print(self.queue_len_df)

    def plot_info(self, figsize=(8, 4), dpi=100):
        """ 画个图

            该方法将会绘制两张子图：

            1. 各个顾客的排队时间（用红色标记出 reneged）
            2. 各个时间点上的队列长度
        """
        fig, axes = plt.subplots(
                2, 1,
                figsize=figsize,
                dpi=dpi)
        colors = []
        for x in self.wait_time_df['is_reneged']:
            if x:
                colors.append('red')
            else:
                colors.append('C0')
        axes[0].bar(
            self.wait_time_df['Customer'],
            self.wait_time_df['wait_time'],
            color=colors)
        axes[0].set_title('wait time')
        axes[0].set_xticklabels([])
        axes[1].plot(
            self.queue_len_df['time'],
            self.queue_len_df['queue_len']
            )
        axes[1].set_title('queue length')
        fig.suptitle('Queue Result')
        fig.tight_layout()
        return fig, axes
